﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using SIR.Common.Condition;
using SIR.Entities.Implementations;
using SIR.Web.API;
using System;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Hosting;

namespace SIR.Web.Services.Tests
{
	[TestClass]
	public class AuthenticationControllerTest
	{
		[TestInitialize]
		public void ConnectToDatabase()
		{
			// Connect to the database before each tests
			DAL.DataServer.Connect(System.Configuration.ConfigurationManager.ConnectionStrings["SIR"].ConnectionString);
			// Establish an HTTP context
			SimpleWorkerRequest request = new SimpleWorkerRequest("", "", "", null, new StringWriter());
			HttpContext context = new HttpContext(request);
			HttpContext.Current = context;
		}

		[TestCleanup]
		public void DisconnectFromTheDatabase()
		{
			// Disconnect after each test
			DAL.DataServer.Disconnect();
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogIn, 1, typeof(AuthenticationController))]
		public void LogIn_EmailRequired()
		{
			var controller = new AuthenticationController();
			// Attempt log in without email or password
			var response = controller.LogIn(new Credentials() { email = string.Empty, password = string.Empty });
			// should with an email required message
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Email address is required to log in", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogIn, 2, typeof(AuthenticationController))]
		public void LogIn_PasswordRequired()
		{
			var controller = new AuthenticationController();
			// attempt log in with no password
			var response = controller.LogIn(new Credentials() { email = "test@password.com", password = string.Empty });
			// should fail with password required message
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Password is required to log in", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogIn, 3, typeof(AuthenticationController))]
		public void LogIn_FailedAuthentication()
		{
			var controller = new AuthenticationController();
			// attempt to log in with an invalid password
			var response = controller.LogIn(new Credentials() { email = "it@sir.com", password = "wrongPassword" });
			// should fail with invalid password message
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Email and/or password is invalid", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogIn, 4, typeof(AuthenticationController))]
		public void LogIn_LockOut()
		{
			var controller = new AuthenticationController();
			Response<User> response;
			for (ushort ii = 0; ii < 3; ++ii)
			{
				// failed log in attempt three times
				response = controller.LogIn(new Credentials() { email = "testing@sir.com", password = "wrongPassword" });
				Assert.IsNotNull(response);
				Assert.IsNull(response.Content);
				Assert.IsFalse(response.Successful);
				Assert.AreEqual("Email and/or password is invalid", response.Message);
			}
			// Using DAL directly to not burden the data service
			var group = new PredicateGroup(new[] 
			{
				new Common.Condition.Predicate("Email", "testing@sir.com", PredicateOperator.EqualTo),
				new Common.Condition.Predicate("InvalidAttempts", 3, PredicateOperator.GreaterThanOrEqualTo)
			}, true);

			var user = SIR.DAL.DataServer.Find<User>(group).Results.FirstOrDefault();
			Assert.IsNotNull(user);
			// Try logging in with the right password to show that it fails
			response = controller.LogIn(new Credentials() { email = "testing@sir.com", password = "password" });
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Email and/or password is invalid", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogIn, 5, typeof(AuthenticationController))]
		public void LogIn_SuccessfulAuthentication()
		{
			var controller = new AuthenticationController();
			// Successful authentication
			var response = controller.LogIn(new Credentials() { email = "test@email.com", password = "password" });
			Assert.IsNotNull(response);
			// Verify message and content
			Assert.AreEqual("Authentication Successful", response.Message);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.Register, 1, typeof(AuthenticationController))]
		public void Register_EmailRequired()
		{
			var controller = new AuthenticationController();
			// Attempt registration within email or password
			var response = controller.Register(new NewUser() { email = string.Empty, password = string.Empty });
			// Verify response unsuccessful and message shows email is required
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Email address is required to register a new user", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.Register, 2, typeof(AuthenticationController))]
		public void Register_PasswordRequired()
		{
			var controller = new AuthenticationController();
			// attempt registration without a password
			var response = controller.Register(new NewUser() { email = "test@sir.com", password = string.Empty });
			// Verify password is required
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Password is required to register a new user", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.Register, 3, typeof(AuthenticationController))]
		public void Register_EmailAlreadyExists()
		{
			var controller = new AuthenticationController();
			// Register with existing user email
			var response = controller.Register(new NewUser() { email = "testing@sir.com", password = "password" });
			// Verify response indicates user already exists
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("The email address has already been associated to another account", response.Message);

		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.Register, 4, typeof(AuthenticationController))]
		public void Register_PasswordDoesNotMeetSecurityRequirements()
		{
			var controller = new AuthenticationController();
			// Register user with weak password
			var response = controller.Register(new NewUser() { email = "test@sir.com", password = "password" });
			// Verify response states email is not strong enough
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Must be at least 8 characters long\nMust contain at least one digit\nMust contain at least one special (non-alphanumeric) character", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.Register, 5, typeof(AuthenticationController))]
		public void Register_NewAccountCreated()
		{
			var controller = new AuthenticationController();
			// Register new user 
			var response = controller.Register(new NewUser() { email = "test@Register_NewAccountCreated.com", password = "P4$$word" });
			// Verify the user was created
			Assert.IsNotNull(response);
			Assert.AreEqual("Registration Successful", response.Message);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
			var condition = new Common.Condition.Predicate("Email", "test@Register_NewAccountCreated.com", PredicateOperator.EqualTo);
			// Find the user in the database to verify the account was created
			var user = SIR.DAL.DataServer.Find<User>(condition).Results.FirstOrDefault();
			Assert.IsNotNull(user);
			// Need to delete the user
			user.IsDeleted = true;
			var result = DAL.DataServer.Save<User>(user);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.LogOut, 1, typeof(AuthenticationController))]
		public void LogOut()
		{
			var controller = new AuthenticationController();
			// Log in with a valid account and verify
			var response = controller.LogIn(new Credentials() { email = "test@email.com", password = "password" });
			Assert.IsNotNull(response);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);

			// Verify the session exits in the DB
			var sessionResults = DAL.DataServer.Find<SirSession>(new Predicate("UserId", response.Content.Id, PredicateOperator.EqualTo));
			Assert.IsNotNull(sessionResults);
			Assert.IsTrue(sessionResults.Results.Count() > 0, "Check results count");
			// Log out
			Assert.IsTrue(controller.LogOut(), "Attempt log out");
			// Verify the session was deleted
			sessionResults = DAL.DataServer.Find<SirSession>(new Predicate("UserId", response.Content.Id, PredicateOperator.EqualTo));
			Assert.IsNotNull(sessionResults);
			Assert.AreEqual(0, sessionResults.Results.Count());
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.ChangePassword, 1, typeof(AuthenticationController))]
		public void ChangePassword_InvalidCurrentPassword()
		{
			var controller = new AuthenticationController();
			// log in with a valid account and verify
			var response = controller.LogIn(new Credentials() { email = "it@sir.com", password = "password" });
			Assert.IsNotNull(response);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
			// Attempt to change the password with invalid current password
			response = controller.ChangePassword(new PasswordChange() { oldPassword = "password2", newPassword = "newPassword" });
			// Verify the password change failed
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Current password was invalid", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.ChangePassword, 2, typeof(AuthenticationController))]
		public void ChangePassword_WeakNewPassword()
		{
			var controller = new AuthenticationController();
			// log in with a valid account
			var response = controller.LogIn(new Credentials() { email = "admin@sir.com", password = "password" });
			Assert.IsNotNull(response);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
			// attempt password change with the right password but a weak new password
			response = controller.ChangePassword(new PasswordChange() { oldPassword = "password", newPassword = "newPassword" });
			// Verify change failed because of weak password
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsFalse(response.Successful);
			Assert.AreEqual("Must be at least 8 characters long\nMust contain at least one digit\nMust contain at least one special (non-alphanumeric) character", response.Message);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.ChangePassword, 3, typeof(AuthenticationController))]
		public void ChangePassword_Successful()
		{
			var controller = new AuthenticationController();
			// log in with a valid account
			var response = controller.LogIn(new Credentials() { email = "it@sir.com", password = "password" });
			Assert.IsNotNull(response);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
			// successful password change
			response = controller.ChangePassword(new PasswordChange() { oldPassword = "password", newPassword = "P4$$word" });
			Assert.IsNotNull(response);
			Assert.AreEqual("Password changed successfully", response.Message);
			Assert.IsNotNull(response.Content);
			Assert.IsTrue(response.Successful);
			// Use DAL to change the password back
			var user = response.Content;
			user.Password = "password";
			var doubleCheck = DAL.DataServer.Save<User>(user);
			Assert.IsTrue(doubleCheck.WasSuccessful);
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.PasswordReset, 1, typeof(AuthenticationController))]
		public void PasswordReset_NonExistentUser()
		{
			var controller = new AuthenticationController();
			// attempt password reset with nonexistent user
			var response = controller.ResetPassword(new Credentials() { email = "PasswordReset_NonExistentUser@sir.com" });
			// response should be successful
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsTrue(response.Successful);
			Assert.AreEqual("Password reset successfully", response.Message);
			// No email should have been sent
			Assert.IsFalse(FindFileWithText("PasswordReset_NonExistentUser@sir.com", "Password Reset",
				(text) => { return text.Contains("Your password has been reset to: "); }));
		}

		[TestMethod]
		[SIRTestCategory(typeof(Area.UserProfile), Area.UserProfile.PasswordReset, 2, typeof(AuthenticationController))]
		public void PasswordReset_Successful()
		{
			var controller = new AuthenticationController();
			// reset password with valid user account
			var response = controller.ResetPassword(new Credentials() { email = "jane@sir.com" });
			// verify successful reset
			Assert.IsNotNull(response);
			Assert.IsNull(response.Content);
			Assert.IsTrue(response.Successful);
			Assert.AreEqual("Password reset successfully", response.Message);
			// verify the email was sent
			Assert.IsTrue(FindFileWithText("jane@sir.com", "Password Reset",
				(text) => { return text.Contains("Your password has been reset to: "); }));
			DAL.DataServer.Save<User>(response.Content);
		}

		/// <summary>
		/// Find an email that was sent to the user
		/// </summary>
		/// <param name="to">Addressee of the email</param>
		/// <param name="subject">Subject</param>
		/// <param name="condition">Function to check the body of the email</param>
		/// <returns></returns>
		private static bool FindFileWithText(string to, string subject, System.Func<string, bool> condition)
		{
			/*To: to@sir.com
			Date: 28 Sep 2013 13:50:02 -0500
			Subject: subject*/
			var files = System.IO.Directory.GetFiles(@"C:\Temp\SIR_SentEmails", "*.eml");
			// For all of the emails
			foreach (var file in files)
			{
				bool correctEmail = false,
					correctSubject = false,
					withinReasonableDate = false,
					passesCondition = false;
				// Read each line
				foreach (var line in System.IO.File.ReadAllLines(file))
				{
					// To line, check addressee
					if (line.StartsWith("To: "))
					{
						if (line.Contains(to))
							correctEmail = true;
						else
							continue;
					}
					// if subject line, check subject
					if (line.StartsWith("Subject: "))
					{
						if (line.Contains(subject))
							correctSubject = true;
						else
							continue;
					}
					// if date line, check the date is less than 10 seconds ago
					if (line.StartsWith("Date: "))
					{
						DateTimeOffset dt;
						if (DateTimeOffset.TryParse(line.Substring(6), out dt))
						{
							if ((DateTimeOffset.Now - dt).TotalSeconds <= 10)
								withinReasonableDate = true;
							else
								continue;
						}
						else
							continue;
					}
					// otherwise check the body if it passes the function
					if (condition(line))
						passesCondition = true;
				}
				// if match all, return true
				if (correctEmail && correctSubject && withinReasonableDate && passesCondition)
					return true;
			}
			// no match
			return false;
		}
	}
}
